import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as chokidar from 'chokidar';
import * as nodePTY from '@terminus-term/node-pty';
import * as childProcess from 'child_process';
var logger;
var MySSHClient = require('./myssh');

abstract class ServerSession {
    isOpen: boolean;
    writable: boolean = false;
    socket: any;
    type: string;
    host: any;
    constructor(socket, type, host) {
        this.socket = socket;
        this.type = type;
        this.host = host;
    }
    emitOutput(data: String): void {
        this.socket.emit("output", data)
    }
    emitError(error: string): void {
        this.socket.emit("error", error)
    }
    emitProgress(progress: string): void {
        this.socket.emit("progress", progress)
    }
    emitDestroy() {
        if (this.isOpen) {
            this.isOpen = false
            this.socket.emit("close")
        }
    }
    emitOpened(data: any = null) {
        this.isOpen = true;
        this.socket.emit("opened", data);
    }
    emitFileChanged(event, file): void {
        this.socket.emit("filechanged", { event: event, file: file });
    }

    abstract start(): void;
    abstract resize(columns: number, rows: number): void;
    abstract write(data: String): void;
    abstract kill(signal?: string): void;
    abstract list(dir: string);
    abstract upload(localPath, remotePath, fileList);
    abstract download(localPath, remotePath, fileList);
    abstract rename(oldname, newname);
    abstract delete(remotePath, fileList);
    abstract mkdir(name);
    abstract readFile(file);
    abstract writeFile(file, data);
    abstract forwardByWindows(host);
    abstract getSysParam();
    getClipboard(): string {
        return '';
    }
}

class PtySession extends ServerSession {
    private pty: any;
    constructor(socket, type, host) {
        super(socket, type, host);
    }
    openLocalShell() {
        return new Promise((resolve) => {
            let pty = nodePTY.spawn('cmd.exe', [], {
                name: 'xterm-color',
                cols: 80,
                rows: 30,
                cwd: process.env.HOME,
                env: process.env
            });
            resolve(pty);
        });
    }
    start(): void {
        this.openLocalShell().then((pty) => {
            this.pty = pty;
            this.emitOpened();
            this.writable = true;
            this.pty.on('data', (data: String) => {
                this.emitOutput(data);
            })

            this.pty.on('exit', () => {
                if (this.isOpen) {
                    this.emitDestroy()
                }
            })

            this.pty.on('close', () => {
                if (this.isOpen) {
                    this.emitDestroy()
                }
            })
        }).catch((err: Error) => {
            this.emitError(err.message);
        });
    }


    resize(cols: number, rows: number): void {
        if (this.isOpen && this.pty._writable) {
            this.pty.resize(cols, rows)
        }
    }

    write(data: String): void {
        if (this.isOpen) {
            if (this.pty._writable) {
                this.pty.write(data)
            } else {
                this.emitDestroy()
            }
        }
    }

    kill(signal?: string): void {
        this.pty.kill(signal)
    }
    list(dir: string) { }
    upload(localPath, remotePath, fileList) { };
    download(localPath, remotePath, fileList) { };
    rename(oldname, newname) { };
    delete(remotePath, fileList) { };
    mkdir(name) { };
    readFile(file) { };
    writeFile(file, data) { };
    forwardByWindows(host) { };
    getSysParam() { };
}
class SshSession extends ServerSession {
    private sshClient: any;
    private sshStream: any;
    constructor(socket, type, host) {
        super(socket, type, host);
    }
    sleep(time) {
        return new Promise((resolve) => setTimeout(resolve, time));
    }
    async executeScripts() {
        let lastHost: any = this.host;
        while (lastHost.child) lastHost = lastHost.child;
        if (lastHost.script) {
            let regex = /^!!sleep (\d+)!!$/;
            await this.sleep(500);
            for (let cmd of lastHost.script.split('\n')) {
                if (regex.test(cmd)) {
                    await this.sleep(Number(RegExp.$1));
                } else {
                    this.sshStream.write(cmd);
                    this.sshStream.write('\n');
                }
            }
        }
    }
    start(): void {
        //this.sshClient = new this.MySSHClient();
        this.sshClient = new MySSHClient(logger);
        this.sshClient.on('banner', (data) => {
            this.emitOutput(data);
        })
        this.sshClient.sshConnect(this.host).then((stream) => {
            this.sshStream = stream;
            this.emitOpened();
            stream.on('data', (data: Buffer) => {
                this.emitOutput(data.toString());
            })
            stream.on('close', (code, signal) => {
                //console.log("ssh closed")
                this.emitError('Connection is broken!');
            })
            this.executeScripts().then(() => this.writable = true);
        }).catch((err: Error) => {
            this.emitError(err.message);
        });
    }

    resize(cols: number, rows: number): void {
        if (this.isOpen) {
            this.sshStream.setWindow(rows, cols)
        }
    }

    write(data: String): void {
        if (this.isOpen) {
            this.sshStream.write(data);
        }
    }

    kill(signal?: string): void {
        this.sshClient.end();
    }
    list(dir: string) { }
    upload(localPath, remotePath, fileList) { };
    download(localPath, remotePath, fileList) { };
    rename(oldname, newname) { };
    delete(remotePath, fileList) { };
    mkdir(name) { };
    readFile(file) { };
    writeFile(file, data) { };
    forwardByWindows(host) { };
    getSysParam() { };
}
class SftpSession extends ServerSession {
    private sshClient: any;
    private sshStream: any;
    constructor(socket, type, host) {
        super(socket, type, host);
    }
    start(): void {
        this.sshClient = new MySSHClient(logger);
        this.sshClient.on('progress', (data) => {
            this.emitProgress(data);
        })
        this.sshClient.sftpConnect(this.host).then(() => {
            this.sshClient.cwd().then((path) => {
                //console.log("cwd:", path)
                this.emitOpened(path);
            })
        }).catch((err: Error) => {
            this.emitError(err.message)
        });
    }
    resize(cols: number, rows: number): void { }

    write(data: String): void { }

    kill(signal?: string): void {
        this.sshClient.end();
    }
    async list(dir: string) {
        try {
            var list = await this.sshClient.list(dir);
        } catch (e) {
            this.emitError(e.message);
        }
        let newList = list.map((item) => {
            return {
                name: item.name,
                type: item.type,
                size: item.size,
                mtime: item.modifyTime,
                checked: false
            }
        })
        return newList;
    }
    async upload(localPath, remotePath, fileList) {
        return this.sshClient.sftpUpload(localPath, remotePath, fileList);
    }
    async download(localPath, remotePath, fileList) {
        return this.sshClient.sftpDownload(localPath, remotePath, fileList);
    }
    async rename(oldname, newname) {
        return this.sshClient.rename(oldname, newname);
    }
    async delete(remotePath, fileList) {
        return this.sshClient.deleteFiles(remotePath, fileList);
    }
    async mkdir(name) {
        return this.sshClient.mkdir(name);
    }
    readFile(file) { };
    writeFile(file, data) { };
    forwardByWindows(host) { };
    getSysParam() { };
}
class LocalSession extends ServerSession {
    watcher: any;
    watchdir: string;
    constructor(socket, type, host) {
        super(socket, type, host);
    }
    start(): void {
        this.emitOpened();
    }
    resize(cols: number, rows: number): void { }

    write(data: String): void { }

    kill(signal?: string): void {
        this.fileSync('');
    }
    fileSync(dir) {
        if (this.watcher) {
            this.watcher.close();
            this.watcher = null;
        }
        if (dir) {
            logger.info('start watch file change:', dir);
            this.watchdir = dir;
            this.watcher = chokidar.watch(dir, { ignoreInitial: true, ignorePermissionErrors: true })
                .on('all', (event, path) => {
                    this.emitFileChanged(event, path);
                }).on('error', (error) => { })
        } else if (this.watchdir) {
            logger.info('stop watch file change:', this.watchdir);
            this.watchdir = '';
        }
    }
    async list(dir: string) {
        var filelist = [];
        try {
            var files = await fs.promises.readdir(dir, { withFileTypes: true });
        } catch (e) {
            this.emitError(e.message);
        }
        for (let file of files) {
            try {
                var states = await fs.promises.stat(path.join(dir, file.name));
                filelist.push({
                    name: file.name,
                    type: states.isDirectory() ? 'd' : '-',
                    size: states.size,
                    mtime: states.mtimeMs,
                    checked: false
                })
            } catch (e) {
                filelist.push({
                    name: file.name,
                    type: '-',
                    size: 0,
                    mtime: null,
                    checked: false
                })
            }
        }
        return filelist;
    }
    async rename(oldname, newname) {
        return fs.promises.rename(oldname, newname);
    }
    async delete(remotePath, fileList) {
        for (var file of fileList) {
            var name = path.join(remotePath, file.name);
            if (file.type == 'd')
                await fs.promises.rmdir(name, { recursive: true });
            else
                await fs.promises.unlink(name);
        }
        return;
    }

    async mkdir(name) {
        return fs.promises.mkdir(name);
    }
    async upload(localPath, remotePath, fileList) { }
    async download(localPath, remotePath, fileList) { }
    async readFile(file) {
        var data;
        try {
            data = await fs.promises.readFile(file);
        } catch (error) {
            data = '';
        }
        return data.toString();
    };
    async writeFile(file, data) {
        await fs.promises.writeFile(file, data);
        return;
    };
    forwardByWindows(host) {
        return new Promise((resolve) => {
            var wmic = childProcess.spawn('wmic', [`/node:${host.winip}`, `/user:${host.winuser}`, `/password:${host.winpass}`, 'process', 'call', 'create', `netsh interface portproxy add v4tov4 listenaddress=${host.winip} listenport=${host.winport} connectaddress=${host.ip} connectport=${host.port}`]);

            wmic.stdout.on('data', function (data) {
                logger.info('usage:' + data);
            });

            wmic.stderr.on('data', function (data) {
                logger.info('stderr:' + data);
            });

            wmic.on('close', function (code) {
                logger.info('child process exited with code :' + code);
                resolve(host);
            });
        })
    };
    _getIPAddress() {
        var interfaces = os.networkInterfaces();
        for (var devName in interfaces) {
            var iface = interfaces[devName];
            for (var i = 0; i < iface.length; i++) {
                var alias = iface[i];
                if (alias.family == 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
                    return alias.address;
                }
            }
        }
        return '127.0.0.1';
    }
    getSysParam() {
        return {
            ip: this._getIPAddress()
        }
    }
}
class ForwardSession extends ServerSession {
    private sshClient: any;
    constructor(socket, type, host) {
        super(socket, type, host);
    }
    start(): void {
        this.sshClient = new MySSHClient(logger);
        this.sshClient.forwardConnect(this.host).then((host) => {
            this.emitOpened(host);
            this.sshClient.on('forward', (data) => {
                this.emitOutput(data);
            })
        }).catch((err: Error) => {
            this.emitError(err.message);
        });
    }

    resize(cols: number, rows: number): void { }

    write(data: String): void { }

    kill(signal?: string): void {
        this.sshClient.end();
    }
    list(dir: string) { }
    upload(localPath, remotePath, fileList) { };
    download(localPath, remotePath, fileList) { };
    rename(oldname, newname) { };
    delete(remotePath, fileList) { };
    mkdir(name) { };
    readFile(file) { };
    writeFile(file, data) { };
    forwardByWindows(host) { };
    getSysParam() { };
}
function appServer(socket) {
    let emitError = (err: Error) => {
        socket.session.emitError(err.message);
    }
    socket.onAny((event, ...args) => {
        //logger.info(`got ${event}`);
    });
    socket.on('start', (type, host) => {
        let session: ServerSession;
        if (type == 'ssh')
            session = new SshSession(socket, type, host);
        else if (type == 'sftp')
            session = new SftpSession(socket, type, host);
        else if (type == 'local-sftp')
            session = new LocalSession(socket, type, host);
        else if (type == 'forward')
            session = new ForwardSession(socket, type, host);
        else if (type == 'local')
            session = new PtySession(socket, type, host);
        logger.info(type, host ? host.ip : '');
        socket.session = session;
        session.start();
    }).on('disconnect', () => {
        let type = socket.session.type;
        let host = socket.session.host;
        logger.info("disconnect", type, host ? host.ip : '');
        socket.session.kill();
    }).on('write', (data) => {
        socket.session.write(data);
    }).on('resize', (cols: number, rows: number) => {
        socket.session.resize(cols, rows);
    }).on('kill', () => {
        socket.session.kill();
    }).on('fileSync', (dir) => {
        socket.session.fileSync(dir);
    }).on('list', (dir) => {
        socket.session.list(dir).then((files) => socket.emit("list", files)).catch(emitError);
    }).on('rename', (oldname, newname) => {
        socket.session.rename(oldname, newname).then(() => socket.emit("rename")).catch(emitError);
    }).on('mkdir', (name) => {
        socket.session.mkdir(name).then(() => socket.emit("mkdir")).catch(emitError);
    }).on('delete', (path, fileList) => {
        socket.session.delete(path, fileList).then(() => socket.emit("delete")).catch(emitError);
    }).on('readFile', (file) => {
        socket.session.readFile(file).then((data) => socket.emit("readFile", data)).catch(emitError);
    }).on('writeFile', (file, data) => {
        socket.session.writeFile(file, data).then((data) => socket.emit("writeFile")).catch(emitError);
    }).on('upload', (localPath, remotePath, fileList) => {
        socket.session.upload(localPath, remotePath, fileList)
            .then(() => socket.emit("upload"))
            .catch((err: Error) => {
                socket.emit("error", err.message);
                socket.emit("upload");
            });
    }).on('download', (localPath, remotePath, fileList) => {
        socket.session.download(localPath, remotePath, fileList)
            .then(() => socket.emit("download"))
            .catch((err: Error) => {
                socket.emit("error", err.message);
                socket.emit("download");
            });
    }).on('forward-by-windows', (host) => {
        socket.session.forwardByWindows(host).then(() => socket.emit("forward-by-windows", host)).catch(emitError);
    }).on('getSysParam', () => {
        socket.emit("getSysParam", socket.session.getSysParam());
    }).on('test', () => socket.emit("test"))
}

import * as http from 'http';
import { AddressInfo } from 'net';
export class AppServer {
    hasConn = false;
    constructor(app, port, plogger, cb = null) {
        logger = plogger;
        const httpServer = http.createServer(app);

        var io = require('socket.io')(httpServer, {
            cors: {
                orgin: '*',
                methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
                allowedHeaders: ['Origin', 'X-Requested-Width', 'Content-Type', 'Accept'],
                credentials: true
            }
        })
        io.on('connect', socket => {
            appServer(socket);
            this.hasConn = true;
        });

        httpServer.listen(port, () => {
            if (cb) {
                let addr = httpServer.address();
                cb((<AddressInfo>addr).port)
            };
        })
    }
}